Repository navigation
feat(mcp): sync MCP servers across enabled CLIs - #521
Conversation
Adds capabilities.mcpConfig to the CLI registry (Claude, Gemini, Codex, OpenCode), an additive src/mcp-sync.ts, GET/POST /api/mcp-sync and a Settings > Agents & CLIs control. Never edits or removes an existing server; backs up each file it changes; reports conflicts. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rted CLIs Formats verified against real agy/gemini/codex mcp add output. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…try tests Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…xpress Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thanks @opticon454, this is a well-built feature. It adds a registry capability ( I tried it against real configs in throwaway HOMEs and found a few things that need fixing before merge. Must fix
Should fix
Smaller items, fine in the same round
Optional: the UI says conflicting names are "left unchanged", but the first CLI's definition is still copied to the CLIs that lack the name ( Separately, I still need to decide on the product side whether Codeman writes other CLIs' own user config at all (it has avoided that so far) and whether this sits behind an opt-in setting. I will post that decision here before you start on the fixes, so nothing is wasted. After that, a push covering items 1 to 4 with tests gets another review. |
Opt-in (mcpSyncEnabled, default OFF; routes 403 until on). Review fixes: - codex TOML read/validated with smol-toml: CRLF, inline tables and command-less tables no longer yield a duplicate [mcp_servers.x]; the new text is re-parsed before writing - null-prototype tables and own-key checks; unsafe names ignored at every level - servers switched off in their own CLI (codex/opencode/antigravity) are not copied - only CLIs that are installed or already have a config file take part - files receiving env/headers are left 0600; symlinked configs are written through - one apply at a time (409), unique tmp files cleaned on failure, failed status - routes set real HTTP status codes; api-reference section; format type single-sourced Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Thanks for the thorough review. Pushed Product decision: it is now opt-in. Must fix
Should fix Smaller items Optional items: the conflict wording in Settings now says the first CLI's definition is copied where the name is missing; the format list is a single While here: the routes previously returned error bodies with HTTP 200; they now set real status codes (403/409/500). CI gate on the branch: typecheck, lint, format, public assets, catalogue and 8561 tests pass. |
|
Heads-up on merge order, so nothing surprises you. I checked my open PRs against each other with trial merges. Every overlap is a textual conflict in a shared registry or list (adjacent insertions), none is a behavioural interaction, but whichever lands first, the others will need a rebase:
I'm not touching this PR for it. As each of #520, #522 and #523 merges I will rebase the others onto master, resolve the conflicts, re-run the full gate and push, so you should not have to deal with any of it. If you would rather take them in a particular order, say so and I will keep to it. #521 is the largest, so landing it first means the smaller PRs rebase once onto it. |
…docs and Settings polish (#521 review) Maintainer merge-time fixes for the MCP server sync (opt-in mcpSyncEnabled, synced, default OFF). M1, parse errors echoed config text (secrets included) into the HTTP response and Settings: smol-toml's TomlError carries a code frame of the offending lines and V8's JSON "Unexpected token" errors quote source. Both catch sites now go through describeMcpSyncError(): a parse failure is reported by line/column only ("not valid TOML (line 3, column 21)", "not valid JSON"), an errno failure by Node's own message (code, syscall, path), the module's own messages via a McpConfigError class, anything else as "unexpected error". Tests put a secret on the broken line (TOML, both JSON message shapes, and a write refused at the re-parse that would have quoted a copied server's env) and assert it is absent from the result and from the route's response body; they fail against the old code. M2, CODEX_HOME / CLAUDE_CONFIG_DIR / XDG_CONFIG_HOME were ignored, so a sync could create a file the CLI never reads and report success: new optional registry field capabilities.mcpConfig.relocation { envVar, path } (registry data, no id branch; schema reuses the env-name and no-traversal path rules). Declared for claude (CLAUDE_CONFIG_DIR, checked in the 2.1.289 binary), codex (CODEX_HOME), opencode (XDG_CONFIG_HOME) and gemini (GEMINI_CLI_HOME, gemini-cli paths.ts); antigravity follows $HOME only (agy 1.1.12 has no relocation var). Resolved from the server process env at call time: absolute moves the file, empty means unset, anything else reports the target with the new status "skipped" plus the reason and writes nothing. Dedupe is now by resolved file. When a caller overrides `home` without passing `env`, process.env is not consulted, and the route tests clear those vars so a CI runner's XDG_CONFIG_HOME can never aim a write outside the temp HOME. M3, feature undocumented: CLAUDE.md Key Patterns paragraph (opt-in, admin-only, additive only, backups, re-parse validation, 0600 for copied secrets, names-only responses with position-only parse errors, capabilities.mcpConfig and relocation), a Settings-Reference row in the wiki, and docs/cli-registry.md + docs/api-reference.md updated for relocation, the "skipped" status and the error policy. Nits: - N1 Preview/Sync before Save: the UI remembers the saved value on open and says "Save settings to turn MCP sync on first" instead of calling the routes; the 403 message also says to turn it on and save. - N2 non-admins in multi-user mode: _applyMcpSyncAdminGate() hides the whole MCP group, called from applyMcpSyncVisibility() and the codeman:me event like the CLI-management gate. - N3 scope chip says "synced". - N4 "(1 servers)" pluralised; the unsupported list only names installed CLIs (route test pins it with a per-test installed set). - N5 McpSyncResult / McpSyncTargetResult moved to src/types/mcp-sync.ts (barrel export); only the route imported them, so no churn. Verified with an isolated instance (throwaway HOME, own instance and tmux socket) and Playwright: chip, save-first message, preview rendering and the admin gate. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Merged and shipped in 1.34.0. Thanks @opticon454! On the product question I left open in my first review: making it opt-in ( I applied the review items while landing (b451b38):
|
What
Sync MCP servers between the agent CLIs. Each CLI keeps its own user-level MCP list in its own file and dialect; this reads every enabled, installed CLI's list and adds any server a CLI is missing from the others.
Opt-in. Settings → Agents & CLIs → MCP servers → "Enable MCP server sync" (
mcpSyncEnabled, synced, default OFF). While it is off,GET/POST /api/mcp-syncanswer403and the Settings controls are hidden. Nothing about it runs or writes anything until a user turns it on.capabilities.mcpConfig({ path, format }, home-relative, schema-guarded against../absolute paths). Declared for Claude, Gemini, Codex, OpenCode and Antigravity. No code branches on a CLI id.src/mcp-sync.ts: the adapters and the sync. Codex'sconfig.tomlis read withsmol-toml(new dependency: BSD-3, zero deps, ~1.9.0).GET /api/mcp-syncpreviews,POST /api/mcp-syncapplies. Admin only in multi-user mode. Documented indocs/api-reference.md.Behaviour
absentand never created.enabled = false, opencodeenabled: false, antigravitydisabled: true), since copying would switch them on elsewhere.<file>.codeman-bak(overwritten by each sync); the new text is re-parsed and every added server must read back as intended before the write; unparseable files (OpenCode JSONC, TOML with a duplicate table) are never written; symlinked configs are written through, not replaced; a file that receivesenv/headersis left0600; one apply at a time (409); temp files unique and removed on failure.unsupported.__proto__/constructor/prototypeare ignored at every level and untrusted-name tables have no prototype.Formats
Checked against what the CLI's own
mcp addwrites (throwaway HOME): Claude, Gemini (url+type), Codex, Antigravity (~/.gemini/config/mcp_config.json,serverUrl). OpenCode follows its documentedmcpshape; it was not installed to verify. After a sync,codex mcp list,gemini mcp listandagy mcp listall load the result.Tests
test/mcp-sync.test.ts,test/mcp-sync-registry.test.ts,test/routes/mcp-sync-routes.test.ts: real CLI output as fixtures; CRLF / inline-table / command-less codex tables; sub-table__proto__andtoString-named servers; disabled servers per dialect; installed gating; permissions; symlinks; concurrency; opt-in 403s; enabled-only; no secrets in responses; multi-user gating; path-traversal rejection. Full CI gate on this branch: typecheck, lint, format, public assets, catalogue and 8561 tests pass.The first revision was tested by hand in the running app by the author. This push (opt-in setting and review fixes) is covered by the test suite and by running the real
codex,geminiandagyCLIs against the synced files in a throwaway HOME; it has not yet been exercised by hand in the Settings UI.🤖 Generated with Claude Code